function initVars() {

    pi = Math.PI;
    ctx = canvas.getContext("2d");
    canvas.width = canvas.clientWidth;
    canvas.height = canvas.clientHeight;
    cx = canvas.width / 2;
    cy = canvas.height / 2;
    playerZ = -25;
    playerX = playerY = playerVX = playerVY = playerVZ = pitch = yaw = pitchV = yawV = 0;
    scale = 600;
    seedTimer = 0; seedInterval = 5, seedLife = 100; gravity = .02;
    seeds = new Array();
    sparkPics = new Array();
    s = "https://cantelope.org/NYE/";
    for (i = 1; i <= 10; ++i) {
        sparkPic = new Image();
        sparkPic.src = s + "spark" + i + ".png";
        sparkPics.push(sparkPic);
    }
    sparks = new Array();
    pow1 = new Audio(s + "pow1.ogg");
    pow2 = new Audio(s + "pow2.ogg");
    pow3 = new Audio(s + "pow3.ogg");
    pow4 = new Audio(s + "pow4.ogg");
    frames = 0;
}

function rasterizePoint(x, y, z) {

    var p, d;
    x -= playerX;
    y -= playerY;
    z -= playerZ;
    p = Math.atan2(x, z);
    d = Math.sqrt(x * x + z * z);
    x = Math.sin(p - yaw) * d;
    z = Math.cos(p - yaw) * d;
    p = Math.atan2(y, z);
    d = Math.sqrt(y * y + z * z);
    y = Math.sin(p - pitch) * d;
    z = Math.cos(p - pitch) * d;
    var rx1 = -1000, ry1 = 1, rx2 = 1000, ry2 = 1, rx3 = 0, ry3 = 0, rx4 = x, ry4 = z, uc = (ry4 - ry3) * (rx2 - rx1) - (rx4 - rx3) * (ry2 - ry1);
    if (!uc) return { x: 0, y: 0, d: -1 };
    var ua = ((rx4 - rx3) * (ry1 - ry3) - (ry4 - ry3) * (rx1 - rx3)) / uc;
    var ub = ((rx2 - rx1) * (ry1 - ry3) - (ry2 - ry1) * (rx1 - rx3)) / uc;
    if (!z) z = .000000001;
    if (ua > 0 && ua < 1 && ub > 0 && ub < 1) {
        return {
            x: cx + (rx1 + ua * (rx2 - rx1)) * scale,
            y: cy + y / z * scale,
            d: Math.sqrt(x * x + y * y + z * z)
        };
    } else {
        return {
            x: cx + (rx1 + ua * (rx2 - rx1)) * scale,
            y: cy + y / z * scale,
            d: -1
        };
    }
}

function spawnSeed() {

    seed = new Object();
    seed.x = -50 + Math.random() * 100;
    seed.y = 25;
    seed.z = -50 + Math.random() * 100;
    seed.vx = .1 - Math.random() * .2;
    seed.vy = -1.5;//*(1+Math.random()/2);
    seed.vz = .1 - Math.random() * .2;
    seed.born = frames;
    seeds.push(seed);
}

function splode(x, y, z) {

    t = 5 + parseInt(Math.random() * 150);
    sparkV = 1 + Math.random() * 2.5;
    type = parseInt(Math.random() * 3);
    switch (type) {
        case 0:
            pic1 = parseInt(Math.random() * 10);
            break;
        case 1:
            pic1 = parseInt(Math.random() * 10);
            do { pic2 = parseInt(Math.random() * 10); } while (pic2 == pic1);
            break;
        case 2:
            pic1 = parseInt(Math.random() * 10);
            do { pic2 = parseInt(Math.random() * 10); } while (pic2 == pic1);
            do { pic3 = parseInt(Math.random() * 10); } while (pic3 == pic1 || pic3 == pic2);
            break;
    }
    for (m = 1; m < t; ++m) {
        spark = new Object();
        spark.x = x; spark.y = y; spark.z = z;
        p1 = pi * 2 * Math.random();
        p2 = pi * Math.random();
        v = sparkV * (1 + Math.random() / 6)
        spark.vx = Math.sin(p1) * Math.sin(p2) * v;
        spark.vz = Math.cos(p1) * Math.sin(p2) * v;
        spark.vy = Math.cos(p2) * v;
        switch (type) {
            case 0: spark.img = sparkPics[pic1]; break;
            case 1:
                spark.img = sparkPics[parseInt(Math.random() * 2) ? pic1 : pic2];
                break;
            case 2:
                switch (parseInt(Math.random() * 3)) {
                    case 0: spark.img = sparkPics[pic1]; break;
                    case 1: spark.img = sparkPics[pic2]; break;
                    case 2: spark.img = sparkPics[pic3]; break;
                }
                break;
        }
        spark.radius = 25 + Math.random() * 50;
        spark.alpha = 1;
        spark.trail = new Array();
        sparks.push(spark);
    }
    switch (parseInt(Math.random() * 4)) {
        case 0: pow = new Audio(s + "pow1.ogg"); break;
        case 1: pow = new Audio(s + "pow2.ogg"); break;
        case 2: pow = new Audio(s + "pow3.ogg"); break;
        case 3: pow = new Audio(s + "pow4.ogg"); break;
    }
    d = Math.sqrt((x - playerX) * (x - playerX) + (y - playerY) * (y - playerY) + (z - playerZ) * (z - playerZ));
    pow.volume = 1.5 / (1 + d / 10);
    pow.play();
}

function doLogic() {

    if (seedTimer < frames) {
        seedTimer = frames + seedInterval * Math.random() * 10;
        spawnSeed();
    }
    for (i = 0; i < seeds.length; ++i) {
        seeds[i].vy += gravity;
        seeds[i].x += seeds[i].vx;
        seeds[i].y += seeds[i].vy;
        seeds[i].z += seeds[i].vz;
        if (frames - seeds[i].born > seedLife) {
            splode(seeds[i].x, seeds[i].y, seeds[i].z);
            seeds.splice(i, 1);
        }
    }
    for (i = 0; i < sparks.length; ++i) {
        if (sparks[i].alpha > 0 && sparks[i].radius > 5) {
            sparks[i].alpha -= .01;
            sparks[i].radius /= 1.02;
            sparks[i].vy += gravity;
            point = new Object();
            point.x = sparks[i].x;
            point.y = sparks[i].y;
            point.z = sparks[i].z;
            if (sparks[i].trail.length) {
                x = sparks[i].trail[sparks[i].trail.length - 1].x;
                y = sparks[i].trail[sparks[i].trail.length - 1].y;
                z = sparks[i].trail[sparks[i].trail.length - 1].z;
                d = ((point.x - x) * (point.x - x) + (point.y - y) * (point.y - y) + (point.z - z) * (point.z - z));
                if (d > 9) {
                    sparks[i].trail.push(point);
                }
            } else {
                sparks[i].trail.push(point);
            }
            if (sparks[i].trail.length > 5) sparks[i].trail.splice(0, 1);
            sparks[i].x += sparks[i].vx;
            sparks[i].y += sparks[i].vy;
            sparks[i].z += sparks[i].vz;
            sparks[i].vx /= 1.075;
            sparks[i].vy /= 1.075;
            sparks[i].vz /= 1.075;
        } else {
            sparks.splice(i, 1);
        }
    }
    p = Math.atan2(playerX, playerZ);
    d = Math.sqrt(playerX * playerX + playerZ * playerZ);
    d += Math.sin(frames / 80) / 1.25;
    t = Math.sin(frames / 200) / 40;
    playerX = Math.sin(p + t) * d;
    playerZ = Math.cos(p + t) * d;
    yaw = pi + p + t;
}

function rgb(col) {

    var r = parseInt((.5 + Math.sin(col) * .5) * 16);
    var g = parseInt((.5 + Math.cos(col) * .5) * 16);
    var b = parseInt((.5 - Math.sin(col) * .5) * 16);
    return "#" + r.toString(16) + g.toString(16) + b.toString(16);
}

function draw() {

    ctx.clearRect(0, 0, cx * 2, cy * 2);

    ctx.fillStyle = "transparent";/*背景色*/
    for (i = -100; i < 100; i += 3) {
        for (j = -100; j < 100; j += 4) {
            x = i; z = j; y = 25;
            point = rasterizePoint(x, y, z);
            if (point.d != -1) {
                size = 250 / (1 + point.d);
                d = Math.sqrt(x * x + z * z);
                a = 0.75 - Math.pow(d / 100, 6) * 0.75;
                if (a > 0) {
                    ctx.globalAlpha = a;
                    ctx.fillRect(point.x - size / 2, point.y - size / 2, size, size);
                }
            }
        }
    }
    ctx.globalAlpha = 1;
    for (i = 0; i < seeds.length; ++i) {
        point = rasterizePoint(seeds[i].x, seeds[i].y, seeds[i].z);
        if (point.d != -1) {
            size = 200 / (1 + point.d);
            ctx.fillRect(point.x - size / 2, point.y - size / 2, size, size);
        }
    }
    point1 = new Object();
    for (i = 0; i < sparks.length; ++i) {
        point = rasterizePoint(sparks[i].x, sparks[i].y, sparks[i].z);
        if (point.d != -1) {
            size = sparks[i].radius * 200 / (1 + point.d);
            if (sparks[i].alpha < 0) sparks[i].alpha = 0;
            if (sparks[i].trail.length) {
                point1.x = point.x;
                point1.y = point.y;
                switch (sparks[i].img) {
                    case sparkPics[0]: ctx.strokeStyle = "#f84"; break;
                    case sparkPics[1]: ctx.strokeStyle = "#84f"; break;
                    case sparkPics[2]: ctx.strokeStyle = "#8ff"; break;
                    case sparkPics[3]: ctx.strokeStyle = "#fff"; break;
                    case sparkPics[4]: ctx.strokeStyle = "#4f8"; break;
                    case sparkPics[5]: ctx.strokeStyle = "#f44"; break;
                    case sparkPics[6]: ctx.strokeStyle = "#f84"; break;
                    case sparkPics[7]: ctx.strokeStyle = "#84f"; break;
                    case sparkPics[8]: ctx.strokeStyle = "#fff"; break;
                    case sparkPics[9]: ctx.strokeStyle = "#44f"; break;
                }
                for (j = sparks[i].trail.length - 1; j >= 0; --j) {
                    point2 = rasterizePoint(sparks[i].trail[j].x, sparks[i].trail[j].y, sparks[i].trail[j].z);
                    if (point2.d != -1) {
                        ctx.globalAlpha = j / sparks[i].trail.length * sparks[i].alpha / 2;
                        ctx.beginPath();
                        ctx.moveTo(point1.x, point1.y);
                        ctx.lineWidth = 1 + sparks[i].radius * 10 / (sparks[i].trail.length - j) / (1 + point2.d);
                        ctx.lineTo(point2.x, point2.y);
                        ctx.stroke();
                        point1.x = point2.x;
                        point1.y = point2.y;
                    }
                }
            }
            ctx.globalAlpha = sparks[i].alpha;
            ctx.drawImage(sparks[i].img, point.x - size / 2, point.y - size / 2, size, size);
        }
    }
}

function frame() {

    if (frames > 100000) {
        seedTimer = 0;
        frames = 0;
    }
    frames++;
    draw();
    doLogic();
    requestAnimationFrame(frame);
}

// window.addEventListener("resize", () => {
//     canvas.width = canvas.clientWidth;
//     canvas.height = canvas.clientHeight;
//     cx = canvas.width / 2;
//     cy = canvas.height / 2;
// });

initVars();
frame();